/*! jQuery UI Virtual Keyboard Version 1.18.12 Author: Jeremy Satterfield Modified: Rob Garrison (Mottie on github) ----------------------------------------- Licensed under the MIT License Caret code modified from jquery.caret.1.02.js Licensed under the MIT License: http://www.opensource.org/licenses/mit-license.php ----------------------------------------- An on-screen virtual keyboard embedded within the browser window which will popup when a specified entry field is focused. The user can then type and preview their input before Accepting or Canceling. As a plugin to jQuery UI styling and theme will automatically match that used by jQuery UI with the exception of the required CSS. Requires: jQuery v1.4.3+ jQuery UI (position utility only) & CSS theme Setup/Usage: Please refer to https://github.com/Mottie/Keyboard/wiki */ /*jshint browser:true, jquery:true, unused:false */ ;(function($){ "use strict"; $.keyboard = function(el, options){ var base = this, o; base.version = '1.18.12'; // Access to jQuery and DOM versions of element base.$el = $(el); base.el = el; // Add a reverse reference to the DOM object base.$el.data("keyboard", base); base.init = function(){ base.options = o = $.extend(true, {}, $.keyboard.defaultOptions, options); // Shift and Alt key toggles, sets is true if a layout has more than one keyset // used for mousewheel message base.shiftActive = base.altActive = base.metaActive = base.sets = base.capsLock = false; base.lastKeyset = [false, false, false]; // [shift, alt, meta] // Class names of the basic key set - meta keysets are handled by the keyname base.rows = [ '', '-shift', '-alt', '-alt-shift' ]; $('').appendTo('body').remove(); base.msie = $('body').hasClass('oldie'); // Old IE flag, used for caret positioning base.allie = $('body').hasClass('ie'); base.inPlaceholder = base.$el.attr('placeholder') || ''; // html 5 placeholder/watermark base.watermark = (typeof(document.createElement('input').placeholder) !== 'undefined' && base.inPlaceholder !== ''); // save default regex (in case loading another layout changes it) base.regex = $.keyboard.comboRegex; // determine if US "." or European "," system being used base.decimal = ( /^\./.test(o.display.dec) ) ? true : false; // convert mouse repeater rate (characters per second) into a time in milliseconds. base.repeatTime = 1000/(o.repeatRate || 20); // delay in ms to prevent mousedown & touchstart from both firing events at the same time o.preventDoubleEventTime = o.preventDoubleEventTime || 100; // flag indication that a keyboard is open base.isOpen = false; // is mousewheel plugin loaded? base.wheel = $.isFunction( $.fn.mousewheel ); // keyCode of keys always allowed to be typed - caps lock, page up & down, end, home, arrow, insert & // delete keys base.alwaysAllowed = [20,33,34,35,36,37,38,39,40,45,46]; base.$keyboard = []; // Check if caret position is saved when input is hidden or loses focus // (*cough* all versions of IE and I think Opera has/had an issue as well base.temp = $('') .appendTo('body').caret(3,3); // Also save caret position of the input if it is locked base.checkCaret = (o.lockInput || base.temp.hide().show().caret().start !== 3 ) ? true : false; base.temp.remove(); base.lastCaret = { start:0, end:0 }; base.temp = [ '', 0, 0 ]; // used when building the keyboard - [keyset element, row, index] // Bind events $.each('initialized beforeVisible visible hidden canceled accepted beforeClose'.split(' '), function(i,f){ if ($.isFunction(o[f])){ base.$el.bind(f + '.keyboard', o[f]); } }); // Close with esc key & clicking outside if (o.alwaysOpen) { o.stayOpen = true; } $(document).bind('mousedown keyup touchstart checkkeyboard '.split(' ').join('.keyboard '), function(e){ if (base.opening) { return; } base.escClose(e); // needed for IE to allow switching between keyboards smoothly if ( e.target && $(e.target).hasClass('ui-keyboard-input') ) { var kb = $(e.target).data('keyboard'); // only trigger on self if (kb === base && kb.options.openOn) { kb.focusOn(); } } }); // Display keyboard on focus base.$el .addClass('ui-keyboard-input ' + o.css.input) .attr({ 'aria-haspopup' : 'true', 'role' : 'textbox' }); // add disabled/readonly class - dynamically updated on reveal if (base.$el.is(':disabled') || (base.$el.attr('readonly') && !base.$el.hasClass('ui-keyboard-lockedinput'))) { base.$el.addClass('ui-keyboard-nokeyboard'); } if (o.openOn) { base.$el.bind(o.openOn + '.keyboard', function(){ base.focusOn(); }); } // Add placeholder if not supported by the browser if (!base.watermark && base.$el.val() === '' && base.inPlaceholder !== '' && base.$el.attr('placeholder') !== '') { base.$el .addClass('ui-keyboard-placeholder') // css watermark style (darker text) .val( base.inPlaceholder ); } base.$el.trigger( 'initialized.keyboard', [ base, base.el ] ); // initialized with keyboard open if (o.alwaysOpen) { base.reveal(); } }; base.setCurrent = function(){ // ui-keyboard-has-focus is applied in case multiple keyboards have alwaysOpen = true and are stacked $('.ui-keyboard-has-focus').removeClass('ui-keyboard-has-focus'); $('.ui-keyboard-input-current').removeClass('ui-keyboard-input-current'); base.$el.addClass('ui-keyboard-input-current'); base.$keyboard.addClass('ui-keyboard-has-focus'); base.isCurrent(true); base.isOpen = true; }; base.isCurrent = function(set){ var cur = $.keyboard.currentKeyboard || false; if (set) { cur = $.keyboard.currentKeyboard = base.el; } else if (set === false && cur === base.el) { cur = $.keyboard.currentKeyboard = ''; } return cur === base.el; }; base.isVisible = function() { return base.$keyboard && base.$keyboard.length ? base.$keyboard.is(":visible") : false; }; base.focusOn = function(){ if (base.$el.is(':visible')) { // caret position is always 0,0 in webkit; and nothing is focused at this point... odd // save caret position in the input to transfer it to the preview // add delay to get correct caret position setTimeout(function(){ // Number inputs don't support selectionStart and selectionEnd if (base.$el.attr('type') != 'number') { base.lastCaret = base.$el.caret(); } }, 20); } if (!base.isVisible()) { clearTimeout(base.timer); base.reveal(); } if (o.alwaysOpen) { base.setCurrent(); } }; base.reveal = function(){ var p, s; base.opening = true; // remove all "extra" keyboards $('.ui-keyboard').not('.ui-keyboard-always-open').remove(); // Don't open if disabled if (base.$el.is(':disabled') || (base.$el.attr('readonly') && !base.$el.hasClass('ui-keyboard-lockedinput'))) { base.$el.addClass('ui-keyboard-nokeyboard'); return; } else { base.$el.removeClass('ui-keyboard-nokeyboard'); } // Unbind focus to prevent recursion - openOn may be empty if keyboard is opened externally if (o.openOn) { base.$el.unbind( o.openOn + '.keyboard' ); } // build keyboard if it doesn't exist; or attach keyboard if it was removed, but not cleared if ( !base.$keyboard || base.$keyboard && ( !base.$keyboard.length || $.contains(document.body, base.$keyboard[0]) ) ) { base.startup(); } // clear watermark if (!base.watermark && base.el.value === base.inPlaceholder) { base.$el .removeClass('ui-keyboard-placeholder') .val(''); } // save starting content, in case we cancel base.originalContent = base.$el.val(); base.$preview.val( base.originalContent ); // disable/enable accept button if (o.acceptValid) { base.checkValid(); } if (o.resetDefault) { base.shiftActive = base.altActive = base.metaActive = false; base.showKeySet(); } // appendLocally && appendTo will now assume the keyboard will be displayed in // its static position; the developer can now position it as desired using the // ".ui-keyboard" class name. if (!o.appendLocally && o.appendTo === 'body') { // basic positioning before it is set by position utility base.$keyboard.css({ position: 'absolute', left: 0, top: 0 }); } // beforeVisible event base.$el.trigger( 'beforeVisible.keyboard', [ base, base.el ] ); base.setCurrent(); // show keyboard base.$keyboard.show(); // adjust keyboard preview window width - save width so IE won't keep expanding (fix issue #6) if (o.usePreview && base.msie) { if (typeof base.width === 'undefined') { base.$preview.hide(); // preview is 100% browser width in IE7, so hide the damn thing base.width = Math.ceil(base.$keyboard.width()); // set input width to match the widest keyboard row base.$preview.show(); } base.$preview.width(base.width); } base.position = o.position; // position after keyboard is visible (required for UI position utility) and appropriately sized if ($.ui && $.ui.position && !$.isEmptyObject(base.position)) { // get single target position || target stored in element data (multiple targets) || default @ element base.position.of = base.position.of || base.$el.data('keyboardPosition') || base.$el; base.position.collision = base.position.collision || 'flipfit flipfit'; base.$keyboard.position(base.position); } base.checkDecimal(); // get preview area line height // add roughly 4px to get line height from font height, works well for font-sizes from 14-36px // needed for textareas base.lineHeight = parseInt( base.$preview.css('lineHeight'), 10) || parseInt(base.$preview.css('font-size') ,10) + 4; if (o.caretToEnd) { s = base.originalContent.length; base.lastCaret = { start: s, end : s }; } // IE caret haxx0rs if (base.allie){ // ensure caret is at the end of the text (needed for IE) s = base.lastCaret.start || base.originalContent.length; p = { start: s, end: s }; // set caret at end of content, if undefined if (!base.lastCaret) { base.lastCaret = p; } // sometimes end = 0 while start is > 0 if (base.lastCaret.end === 0 && base.lastCaret.start > 0) { base.lastCaret.end = base.lastCaret.start; } // IE will have start -1, end of 0 when not focused (see demo: http://jsfiddle.net/Mottie/fgryQ/3/) if (base.lastCaret.start < 0) { base.lastCaret = p; } } // opening keyboard flag; delay allows switching between keyboards without immediately closing // the keyboard setTimeout(function(){ base.opening = false; if (o.initialFocus) { base.$preview.focus().caret( base.lastCaret ); } base.$el.trigger( 'visible.keyboard', [ base, base.el ] ); }, 10); // return base to allow chaining in typing extension return base; }; base.startup = function(){ if ( !(base.$keyboard && base.$keyboard.length) ) { // custom layout - create a unique layout name based on the hash if (o.layout === "custom") { o.layoutHash = 'custom' + base.customHash(); } base.layout = o.layout === "custom" ? o.layoutHash : o.layout; if (typeof $.keyboard.builtLayouts[base.layout] === 'undefined') { if ($.isFunction(o.create)) { o.create(base); } if (!base.$keyboard.length) { base.buildKeyboard(); } } base.$keyboard = $.keyboard.builtLayouts[base.layout].$keyboard.clone(); // build preview display if (o.usePreview) { base.$preview = base.$el.clone(false) .removeAttr('id') .removeClass('ui-keyboard-placeholder ui-keyboard-input') .addClass('ui-keyboard-preview ' + o.css.input) .removeAttr('aria-haspopup') .attr('tabindex', '-1') .show(); // for hidden inputs // Switch the number input fields to text so the caret positioning will work again if (base.$preview.attr('type') == 'number') { base.$preview.attr('type', 'text'); } // build preview container and append preview display $('
') .addClass('ui-keyboard-preview-wrapper') .append(base.$preview) .prependTo(base.$keyboard); } else { // No preview display, use element and reposition the keyboard under it. base.$preview = base.$el; if (!$.isEmptyObject(base.position)) { o.position.at = o.position.at2; } } } base.preview = base.$preview[0]; base.$decBtn = base.$keyboard.find('.ui-keyboard-dec'); // add enter to allowed keys; fixes #190 if (o.enterNavigation || base.el.tagName === "TEXTAREA") { base.alwaysAllowed.push(13); } if (o.lockInput) { base.$preview.addClass('ui-keyboard-lockedinput').attr({ 'readonly': 'readonly'}); } base.bindKeyboard(); base.$keyboard.appendTo( o.appendLocally ? base.$el.parent() : o.appendTo || 'body' ); base.bindKeys(); // adjust with window resize if ($.ui && $.ui.position && !$.isEmptyObject(base.position)) { $(window).bind('resize.keyboard', function(){ if (base.isVisible()) { base.$keyboard.position(base.position); } }); } }; base.bindKeyboard = function(){ var layout = $.keyboard.builtLayouts[base.layout]; base.$preview .unbind('keypress keyup keydown mouseup touchend '.split(' ').join('.keyboard ')) .bind('keypress.keyboard', function(e){ var k = base.lastKey = String.fromCharCode(e.charCode || e.which); base.$lastKey = []; // not a virtual keyboard key if (base.checkCaret) { base.lastCaret = base.$preview.caret(); } // update caps lock - can only do this while typing =( base.capsLock = (((k >= 65 && k <= 90) && !e.shiftKey) || ((k >= 97 && k <= 122) && e.shiftKey)) ? true : false; // restrict input - keyCode in keypress special keys: // see http://www.asquare.net/javascript/tests/KeyCode.html if (o.restrictInput) { // allow navigation keys to work - Chrome doesn't fire a keypress event (8 = bksp) if ( (e.which === 8 || e.which === 0) && $.inArray( e.keyCode, base.alwaysAllowed ) ) { return; } if ($.inArray(k, layout.acceptedKeys) === -1) { e.preventDefault(); } // quick key check } else if ( (e.ctrlKey || e.metaKey) && (e.which === 97 || e.which === 99 || e.which === 118 || (e.which >= 120 && e.which <=122)) ) { // Allow select all (ctrl-a:97), copy (ctrl-c:99), paste (ctrl-v:118) & cut (ctrl-x:120) & // redo (ctrl-y:121)& undo (ctrl-z:122); meta key for mac return; } // Mapped Keys - allows typing on a regular keyboard and the mapped key is entered // Set up a key in the layout as follows: "m(a):label"; m = key to map, (a) = actual keyboard key // to map to (optional), ":label" = title/tooltip (optional) // example: \u0391 or \u0391(A) or \u0391:alpha or \u0391(A):alpha if (layout.hasMappedKeys) { if (layout.mappedKeys.hasOwnProperty(k)){ base.lastKey = layout.mappedKeys[k]; base.insertText( base.lastKey ); e.preventDefault(); } } base.checkMaxLength(); }) .bind('keyup.keyboard', function(e){ switch (e.which) { // Insert tab key case 9 : // Added a flag to prevent from tabbing into an input, keyboard opening, then adding the tab to the keyboard preview // area on keyup. Sadly it still happens if you don't release the tab key immediately because keydown event auto-repeats if (base.tab && o.tabNavigation && !o.lockInput) { base.shiftActive = e.shiftKey; // when switching inputs, the tab keyaction returns false var notSwitching = $.keyboard.keyaction.tab(base); base.tab = false; if (!notSwitching) { return false; } } else { e.preventDefault(); } break; // Escape will hide the keyboard case 27: base.close(); return false; } // throttle the check combo function because fast typers will have an incorrectly positioned caret clearTimeout(base.throttled); base.throttled = setTimeout(function(){ // fix error in OSX? see issue #102 if (base.isVisible()) { base.checkCombos(); } }, 100); base.checkMaxLength(); // change callback is no longer bound to the input element as the callback could be // called during an external change event with all the necessary parameters (issue #157) if ($.isFunction(o.change)){ o.change( $.Event("change"), base, base.el ); } base.$el.trigger( 'change.keyboard', [ base, base.el ] ); }) .bind('keydown.keyboard', function(e){ switch (e.which) { case 8 : $.keyboard.keyaction.bksp(base, null, e); e.preventDefault(); break; // prevent tab key from leaving the preview window case 9 : if (o.tabNavigation) { // allow tab to pass through - tab to next input/shift-tab for prev base.tab = true; return false; } else { base.tab = true; // see keyup comment above return false; } break; // adding a break here to make jsHint happy case 13: $.keyboard.keyaction.enter(base, null, e); break; // Show capsLock case 20: base.shiftActive = base.capsLock = !base.capsLock; base.showKeySet(this); break; case 86: // prevent ctrl-v/cmd-v if (e.ctrlKey || e.metaKey) { if (o.preventPaste) { e.preventDefault(); return; } base.checkCombos(); // check pasted content } break; } }) .bind('mouseup.keyboard touchend.keyboard', function(){ if (base.checkCaret) { base.lastCaret = base.$preview.caret(); } }); // prevent keyboard event bubbling base.$keyboard.bind('mousedown.keyboard click.keyboard touchstart.keyboard', function(e){ e.stopPropagation(); if (!base.isCurrent()) { base.reveal(); $(document).trigger('checkkeyboard.keyboard'); } }); // If preventing paste, block context menu (right click) if (o.preventPaste){ base.$preview.bind('contextmenu.keyboard', function(e){ e.preventDefault(); }); base.$el.bind('contextmenu.keyboard', function(e){ e.preventDefault(); }); } }; base.bindKeys = function(){ var allEvents = (o.keyBinding + ' repeater mouseenter mouseleave touchstart mousewheel ' + 'mouseup click ').split(' ').join('.keyboard ') + ('mouseleave mousedown touchstart ' + 'touchend touchmove touchcancel ').split(' ').join('.kb '); base.$allKeys = base.$keyboard.find('button.ui-keyboard-button') .unbind(allEvents) .bind(o.keyBinding.split(' ').join('.keyboard ') + '.keyboard repeater.keyboard', function(e){ // prevent errors when external triggers attempt to "type" - see issue #158 if (!base.$keyboard.is(":visible")){ return false; } // 'key', { action: doAction, original: n, curtxt : n, curnum: 0 } var txt, $this = $(this), action = $this.attr('data-action'), // prevent mousedown & touchstart from both firing events at the same time - see #184 timer = new Date().getTime(); // don't split colon key. Fixes #264 action = action === ':' ? ':' : action.split(':')[0]; if (timer - (base.lastEventTime || 0) < o.preventDoubleEventTime) { return; } base.lastEventTime = timer; base.$preview.focus(); base.$lastKey = $this; base.lastKey = $this.attr('data-curtxt'); // Start caret in IE when not focused (happens with each virtual keyboard button click if (base.checkCaret) { base.$preview.caret( base.lastCaret ); } if (action.match('meta')) { action = 'meta'; } if ($.keyboard.keyaction.hasOwnProperty(action) && $(this).hasClass('ui-keyboard-actionkey')) { // stop processing if action returns false (close & cancel) if ($.keyboard.keyaction[action](base,this,e) === false) { return false; } } else if (typeof action !== 'undefined') { txt = base.lastKey = (base.wheel && !$(this).hasClass('ui-keyboard-actionkey')) ? base.lastKey : action; base.insertText(txt); if (!base.capsLock && !o.stickyShift && !e.shiftKey) { base.shiftActive = false; base.showKeySet({ name : "meta2" }); } } // set caret if caret moved by action function; also, attempt to fix issue #131 base.$preview.focus().caret( base.lastCaret ); base.checkCombos(); base.checkMaxLength(); if ($.isFunction(o.change)){ o.change( $.Event("change"), base, base.el ); } base.$el.trigger( 'change.keyboard', [ base, base.el ] ); e.preventDefault(); }) // Change hover class and tooltip .bind('mouseenter.keyboard mouseleave.keyboard touchstart.keyboard', function(e){ if (!base.isCurrent()) { return; } var $this = $(this), txt = $this.data('layers') || base.getLayers( $this ); // remove duplicates $this.data('layers', txt = $.grep(txt, function(v, k){ return $.inArray(v, txt) === k; }) ); if ((e.type === 'mouseenter' || e.type === 'touchstart') && base.el.type !== 'password' && !$this.hasClass(o.css.buttonDisabled) ){ $this .addClass(o.css.buttonHover) .attr('title', function(i,t){ // show mouse wheel message return (base.wheel && t === '' && base.sets && txt.length > 1 && e.type !== 'touchstart') ? o.wheelMessage : t; }); } if (e.type === 'mouseleave'){ $this.data({ 'curtxt' : $this.data('original'), 'curnum' : 0 }); $this // needed or IE flickers really bad .removeClass( (base.el.type === 'password') ? '' : o.css.buttonHover) .attr('title', function(i,t){ return (t === o.wheelMessage) ? '' : t; }) .find('span').html( $this.data('original') ); // restore original button text } }) // Allow mousewheel to scroll through other key sets of the same key .bind('mousewheel.keyboard', function(e, delta){ if (base.wheel) { // deltaY used by newer versions of mousewheel plugin delta = delta || e.deltaY; var n, txt, $this = $(this); txt = $this.data('layers') || base.getLayers( $this ); if (txt.length > 1) { n = $this.data('curnum') + (delta > 0 ? -1 : 1); if (n > txt.length-1) { n = 0; } if (n < 0) { n = txt.length-1; } } else { n = 0; } $this.data({ 'curnum' : n, 'layers' : txt, 'curtxt' : txt[n] }); $this.find('span').html( txt[n] ); return false; } }) // using "kb" namespace for mouse repeat functionality to keep it separate // I need to trigger a "repeater.keyboard" to make it work .bind('mouseup.keyboard mouseleave.kb touchend.kb touchmove.kb touchcancel.kb', function(e){ if (/(mouseleave|touchend|touchcancel)/.test(e.type)) { $(this).removeClass(o.css.buttonHover); // needed for touch devices } else { if (base.isVisible() && base.isCurrent()) { base.$preview.focus(); } if (base.checkCaret) { base.$preview.caret( base.lastCaret ); } } base.mouseRepeat = [false,'']; clearTimeout(base.repeater); // make sure key repeat stops! return false; }) // prevent form submits when keyboard is bound locally - issue #64 .bind('click.keyboard', function(){ return false; }) // no mouse repeat for action keys (shift, ctrl, alt, meta, etc) .not('.ui-keyboard-actionkey') // mouse repeated action key exceptions .add('.ui-keyboard-tab, .ui-keyboard-bksp, .ui-keyboard-space, .ui-keyboard-enter', base.$keyboard) .bind('mousedown.kb touchstart.kb', function(){ if (o.repeatRate !== 0) { var key = $(this); base.mouseRepeat = [true, key]; // save the key, make sure we are repeating the right one (fast typers) setTimeout(function() { if (base.mouseRepeat[0] && base.mouseRepeat[1] === key) { base.repeatKey(key); } }, o.repeatDelay); } return false; }); }; // Insert text at caret/selection - thanks to Derek Wickwire for fixing this up! base.insertText = function(txt){ var bksp, t, h, // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE). val = base.$preview.val(), pos = base.$preview.caret(), scrL = base.$preview.scrollLeft(), scrT = base.$preview.scrollTop(), len = val.length; // save original content length // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea // is still difficult // in IE, pos.end can be zero after input loses focus if (pos.end < pos.start) { pos.end = pos.start; } if (pos.start > len) { pos.end = pos.start = len; } if (base.preview.tagName === 'TEXTAREA') { // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine) if (base.msie && val.substr(pos.start, 1) === '\n') { pos.start += 1; pos.end += 1; } // Set scroll top so current text is in view - needed for virtual keyboard typing, not manual typing // this doesn't appear to work correctly in Opera h = (val.split('\n').length - 1); base.preview.scrollTop = (h>0) ? base.lineHeight * h : scrT; } bksp = (txt === 'bksp' && pos.start === pos.end) ? true : false; txt = (txt === 'bksp') ? '' : txt; t = pos.start + (bksp ? -1 : txt.length); scrL += parseInt(base.$preview.css('fontSize'),10) * (txt === 'bksp' ? -1 : 1); base.$preview .val( base.$preview.val().substr(0, pos.start - (bksp ? 1 : 0)) + txt + base.$preview.val().substr(pos.end) ) .scrollLeft(scrL) .caret(t, t); base.lastCaret = { start: t, end: t }; // save caret in case of bksp base.startup(); }; // check max length base.checkMaxLength = function(){ var t, p = base.$preview.val(); if (o.maxLength !== false && p.length > o.maxLength) { t = Math.min(base.$preview.caret().start, o.maxLength); base.$preview.val( p.substring(0, o.maxLength) ); // restore caret on change, otherwise it ends up at the end. base.$preview.caret( t, t ); base.lastCaret = { start: t, end: t }; } if (base.$decBtn.length) { base.checkDecimal(); } }; // mousedown repeater base.repeatKey = function(key){ key.trigger('repeater.keyboard'); if (base.mouseRepeat[0]) { base.repeater = setTimeout(function() { base.repeatKey(key); }, base.repeatTime); } }; base.showKeySet = function(el){ var key = '', toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0); if (!base.shiftActive) { base.capsLock = false; } // check meta key set if (base.metaActive) { // the name attribute contains the meta set # "meta99" key = (el && el.name && /meta/.test(el.name)) ? el.name : ''; // save active meta keyset name if (key === '') { key = (base.metaActive === true) ? '' : base.metaActive; } else { base.metaActive = key; } // if meta keyset doesn't have a shift or alt keyset, then show just the meta key set if ( (!o.stickyShift && base.lastKeyset[2] !== base.metaActive) || ( (base.shiftActive || base.altActive) && !base.$keyboard.find('.ui-keyboard-keyset-' + key + base.rows[toShow]).length) ) { base.shiftActive = base.altActive = false; } } else if (!o.stickyShift && base.lastKeyset[2] !== base.metaActive && base.shiftActive) { // switching from meta key set back to default, reset shift & alt if using stickyShift base.shiftActive = base.altActive = false; } toShow = (base.shiftActive ? 1 : 0) + (base.altActive ? 2 : 0); key = (toShow === 0 && !base.metaActive) ? '-default' : (key === '') ? '' : '-' + key; if (!base.$keyboard.find('.ui-keyboard-keyset' + key + base.rows[toShow]).length) { // keyset doesn't exist, so restore last keyset settings base.shiftActive = base.lastKeyset[0]; base.altActive = base.lastKeyset[1]; base.metaActive = base.lastKeyset[2]; return; } base.$keyboard .find('.ui-keyboard-alt, .ui-keyboard-shift, .ui-keyboard-actionkey[class*=meta]') .removeClass(o.css.buttonAction).end() .find('.ui-keyboard-alt')[(base.altActive) ? 'addClass' : 'removeClass'](o.css.buttonAction).end() .find('.ui-keyboard-shift')[(base.shiftActive) ? 'addClass' : 'removeClass'](o.css.buttonAction).end() .find('.ui-keyboard-lock')[(base.capsLock) ? 'addClass' : 'removeClass'](o.css.buttonAction).end() .find('.ui-keyboard-keyset').hide().end() .find('.ui-keyboard-keyset' + key + base.rows[toShow]).show().end() .find('.ui-keyboard-actionkey.ui-keyboard' + key).addClass(o.css.buttonAction); base.lastKeyset = [ base.shiftActive, base.altActive, base.metaActive ]; }; // check for key combos (dead keys) base.checkCombos = function(){ if (!base.isVisible()) { return base.$preview.val(); } var i, r, t, t2, // use base.$preview.val() instead of base.preview.value (val.length includes carriage returns in IE). val = base.$preview.val(), pos = base.$preview.caret(), layout = $.keyboard.builtLayouts[base.layout], len = val.length; // save original content length // silly IE caret hacks... it should work correctly, but navigating using arrow keys in a textarea // is still difficult // in IE, pos.end can be zero after input loses focus if (pos.end < pos.start) { pos.end = pos.start; } if (pos.start > len) { pos.end = pos.start = len; } // This makes sure the caret moves to the next line after clicking on enter (manual typing works fine) if (base.msie && val.substr(pos.start, 1) === '\n') { pos.start += 1; pos.end += 1; } if (o.useCombos) { // keep 'a' and 'o' in the regex for ae and oe ligature (æ,œ) // thanks to KennyTM: http://stackoverflow.com/q/4275077 // original regex /([`\'~\^\"ao])([a-z])/mig moved to $.keyboard.comboRegex if (base.msie) { // old IE may not have the caret positioned correctly, so just check the whole thing val = val.replace(base.regex, function(s, accent, letter){ return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s; }); // prevent combo replace error, in case the keyboard closes - see issue #116 } else if (base.$preview.length) { // Modern browsers - check for combos from last two characters left of the caret t = pos.start - (pos.start - 2 >= 0 ? 2 : 0); // target last two characters base.$preview.caret(t, pos.end); // do combo replace t2 = (base.$preview.caret().text || '').replace(base.regex, function(s, accent, letter){ return (o.combos.hasOwnProperty(accent)) ? o.combos[accent][letter] || s : s; }); // add combo back base.$preview.val( base.$preview.caret().replace(t2) ); val = base.$preview.val(); } } // check input restrictions - in case content was pasted if (o.restrictInput && val !== '') { t = val; r = layout.acceptedKeys.length; for (i=0; i < r; i++){ if (t === '') { continue; } t2 = layout.acceptedKeys[i]; if (val.indexOf(t2) >= 0) { // escape out all special characters if (/[\[|\]|\\|\^|\$|\.|\||\?|\*|\+|\(|\)|\{|\}]/g.test(t2)) { t2 = '\\' + t2; } t = t.replace( (new RegExp(t2, "g")), ''); } } // what's left over are keys that aren't in the acceptedKeys array if (t !== '') { val = val.replace(t, ''); } } // save changes, then reposition caret pos.start += val.length - len; pos.end += val.length - len; base.$preview.val(val); base.$preview.caret(pos.start, pos.end); // calculate current cursor scroll location and set scrolltop to keep it in view // find row, multiply by font-size base.preview.scrollTop = base.lineHeight * (val.substring(0, pos.start).split('\n').length - 1); base.lastCaret = { start: pos.start, end: pos.end }; if (o.acceptValid) { base.checkValid(); } return val; // return text, used for keyboard closing section }; // Toggle accept button classes, if validating base.checkValid = function(){ var valid = true; if (o.validate && typeof o.validate === "function") { valid = o.validate(base, base.$preview.val(), false); } // toggle accept button classes; defined in the css base.$keyboard.find('.ui-keyboard-accept') [valid ? 'removeClass' : 'addClass']('ui-keyboard-invalid-input') [valid ? 'addClass' : 'removeClass']('ui-keyboard-valid-input'); }; // Decimal button for num pad - only allow one (not used by default) base.checkDecimal = function(){ // Check US "." or European "," format if ( ( base.decimal && /\./g.test(base.preview.value) ) || ( !base.decimal && /\,/g.test(base.preview.value) ) ) { base.$decBtn .attr({ 'disabled': 'disabled', 'aria-disabled': 'true' }) .removeClass(o.css.buttonDefault + ' ' + o.css.buttonHover) .addClass(o.css.buttonDisabled); } else { base.$decBtn .removeAttr('disabled') .attr({ 'aria-disabled': 'false' }) .addClass(o.css.buttonDefault) .removeClass(o.css.buttonDisabled); } }; // get other layer values for a specific key base.getLayers = function(el){ var key, keys; key = el.attr('data-pos'); keys = el.closest('.ui-keyboard').find('button[data-pos="' + key + '"]').map(function(){ // added '> span' because jQuery mobile adds multiple spans inside the button return $(this).find('> span').html(); }).get(); return keys; }; // Go to next or prev inputs // goToNext = true, then go to next input; if false go to prev // isAccepted is from autoAccept option or true if user presses shift-enter base.switchInput = function(goToNext, isAccepted){ if (typeof o.switchInput === "function") { o.switchInput(base, goToNext, isAccepted); } else { base.$keyboard.hide(); var kb, stopped = false, all = $('button, input, textarea, a').filter(':visible'), indx = all.index(base.$el) + (goToNext ? 1 : -1); base.$keyboard.show(); if (indx > all.length - 1) { stopped = o.stopAtEnd; indx = 0; // go to first input } if (indx < 0) { stopped = o.stopAtEnd; indx = all.length - 1; // stop or go to last } if (!stopped) { isAccepted = base.close(isAccepted); if (!isAccepted) { return; } kb = all.eq(indx).data('keyboard'); if (kb && kb.options.openOn.length) { kb.focusOn(); } else { all.eq(indx).focus(); } } } return false; }; // Close the keyboard, if visible. Pass a status of true, if the content was accepted // (for the event trigger). base.close = function(accepted){ if (base.isOpen) { clearTimeout(base.throttled); var val = (accepted) ? base.checkCombos() : base.originalContent; // validate input if accepted if (accepted && o.validate && typeof(o.validate) === "function" && !o.validate(base, val, true)) { val = base.originalContent; accepted = false; if (o.cancelClose) { return; } } base.isCurrent(false); base.isOpen = false; // update value for always open keyboards base.$preview.val(val); base.$el .removeClass('ui-keyboard-input-current ui-keyboard-autoaccepted') // add "ui-keyboard-autoaccepted" to inputs - see issue #66 .addClass( (accepted || false) ? accepted === true ? '' : 'ui-keyboard-autoaccepted' : '' ) .trigger( (o.alwaysOpen) ? '' : 'beforeClose.keyboard', [ base, base.el, (accepted || false) ] ) .val( val ) .scrollTop( base.el.scrollHeight ) .trigger( ((accepted || false) ? 'accepted.keyboard' : 'canceled.keyboard'), [ base, base.el ] ) .trigger( (o.alwaysOpen) ? 'inactive.keyboard' : 'hidden.keyboard', [ base, base.el ] ) .blur(); if (o.openOn) { // rebind input focus - delayed to fix IE issue #72 base.timer = setTimeout(function(){ base.$el.bind( o.openOn + '.keyboard', function(){ base.focusOn(); }); // remove focus from element (needed for IE since blur doesn't seem to work) if ($(':focus')[0] === base.el) { base.$el.blur(); } }, 500); } if (!o.alwaysOpen && base.$keyboard) { // free up memory base.$keyboard.remove(); base.$keyboard = []; } if (!base.watermark && base.el.value === '' && base.inPlaceholder !== '') { base.$el .addClass('ui-keyboard-placeholder') .val(base.inPlaceholder); } // trigger default change event - see issue #146 base.$el.trigger('change'); } return !!accepted; }; base.accept = function(){ return base.close(true); }; base.escClose = function(e){ if ( e && e.type === 'keyup' ) { return ( e.which === 27 ) ? base.close() : ''; } // keep keyboard open if alwaysOpen or stayOpen is true - fixes mutliple always open keyboards or // single stay open keyboard if ( !base.isOpen ) { return; } // ignore autoaccept if using escape - good idea? if ( !base.isCurrent() && base.isOpen || base.isOpen && e.target !== base.el && !o.stayOpen) { // stop propogation in IE - an input getting focus doesn't open a keyboard if one is already open if ( base.allie ) { e.preventDefault(); } // send "true" instead of a true (boolean), the input won't get a "ui-keyboard-autoaccepted" // class name - see issue #66 base.close( o.autoAccept ? 'true' : false ); } }; // Build default button base.keyBtn = $('